Swadhin Biswas feat(api): add runner, analytics, packages, and security services · 3/6/2026
28f7954
85 lines · 2.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import type { APIRoute } from "astro";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { and, eq } from "drizzle-orm";
import { z } from "zod";
import { getDatabase, schema } from "@/db";
import { badRequest, notFound, parseQuery } from "@/lib/api";
import { withErrorHandler } from "@/lib/errors";
import { canReadRepo } from "@/lib/permissions";
import { exportMetrics } from "@/lib/analytics-advanced";

const querySchema = z.object({
  format: z.enum(["json", "csv", "prometheus"]).default("json"),
  metrics: z.string().default("pr_velocity,review_time,cycle_time"),
  start: z.string().optional(),
  end: z.string().optional(),
});

async function resolveRepository(owner: string, repoName: string) {
  const db = getDatabase() as NodePgDatabase<typeof schema>;
  const repoOwner = await db.query.users.findFirst({
    where: eq(schema.users.username, owner),
  });
  if (!repoOwner) return null;
  return db.query.repositories.findFirst({
    where: and(
      eq(schema.repositories.ownerId, repoOwner.id),
      eq(schema.repositories.name, repoName)
    ),
  });
}

export const GET: APIRoute = withErrorHandler(async ({ params, url, locals }) => {
  const owner = params.owner;
  const repoName = params.repo;
  if (!owner || !repoName) return badRequest("Missing route parameters");

  const parsed = parseQuery(url, querySchema);
  if ("error" in parsed) return parsed.error;

  const repository = await resolveRepository(owner, repoName);
  if (!repository) return notFound("Repository not found");
  if (!(await canReadRepo(locals.user?.id, repository, { isAdmin: locals.user?.isAdmin }))) {
    return notFound("Repository not found");
  }

  const endDate = parsed.data.end ? new Date(parsed.data.end) : new Date();
  const startDate = parsed.data.start
    ? new Date(parsed.data.start)
    : new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1000);
  if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
    return badRequest("Invalid start/end date");
  }

  const metricsRaw = parsed.data.metrics || "pr_velocity,review_time,cycle_time";
  const metrics = metricsRaw
    .split(",")
    .map((m) => m.trim())
    .filter(Boolean);
  if (metrics.length === 0) return badRequest("At least one metric is required");

  const format = parsed.data.format || "json";
  const content = await exportMetrics({
    repositoryId: repository.id,
    metrics,
    startDate,
    endDate,
    format,
  });

  const contentType =
    format === "csv"
      ? "text/csv; charset=utf-8"
      : format === "prometheus"
        ? "text/plain; version=0.0.4; charset=utf-8"
        : "application/json; charset=utf-8";

  return new Response(content, {
    status: 200,
    headers: {
      "Content-Type": contentType,
      "Content-Disposition": `attachment; filename=\"metrics-${repository.name}.${format === "prometheus" ? "txt" : format}\"`,
    },
  });
});